/*----------------------------------------------------------------------------
 | Copyright (c) 2009 Matthew B. Brophy (matt@brophy.org).  
 | Liscense: MIT License (http://www.opensource.org/licenses/mit-license.php)
 |
 | $Id$
 -----------------------------------------------------------------------------*/

if (typeof org == "undefined" || 
    org == null) { org = {}; }
if (typeof org.brophy == "undefined" || 
    org.brophy == null) { org.brophy = {}; }
if (typeof org.brophy.js == "undefined" || 
    org.brophy.js == null) { org.brophy.js = {}; }

/** 
 * @namespace 
 * @description Dragging namespace in the org.brophy.js JavaScript library 
 * @requires    This library requires the <a href="http://www.mochikit.com">
 *              MochiKit</a> JavaScript library 
 *              (version 1.4+).
 */
org.brophy.js.drag = {};

/**
 * @constructor
 * @description 
 *   The Dragger class lets you easily add drag and drop 
 *   funcitonality to any element.  Simply create a new Dragger()
 *   object with whatever options you like, and then call the 
 *   Dragger.setElementDraggable() on all the elements you 
 *   would like to be able to drag.
 *   <br><br>
 *   In the options below that are functions, you can use the methods
 *   this.getOriginalNode() and this.getTargetNode() to retrieve the
 *   node we started dragging and the node we are actually dragging,
 *   respectively, as well as this.getEvent() to get the event that 
 *   was fired.
 *
 * @param options
 *    This is an object that can have the following options, and 
 *    their (type) and [default] values:<br/><br/>
 *    <ul>
 *      <li><b>cloneNode</b> (boolean) [false] </li>
 *        Should we clone the node we clicked on and drag that instead?
 *      <br><br>
 *      <li><b>getClonedNode</b> (function) [null] </li>
 *        Advanced function to create a different node to drag instead 
 *        of the original node we clicked on.  <br>
 *         - Takes no parameters<br>
 *         - Should return the new node to drag
 *      <br><br>
 *      <li><b>removeElement</b> (boolean) [false] </li>
 *      Should we remove the dragged element from the DOM when we finish? 
 *      <br><br> 
 *      <li><b>opacity</b> (float) [0.5] </li>
 *      Opacity value between 0 and 1 for the dragged element 
 *      <br><br>
 *      <li><b>snap</b> (integer) [1] </li>
 *      While dragging, snap the element every 'n' pixels from it's
 *      original location
 *      <br><br>
 *      <li><b>disableVertical</b> (boolean) [false] </li>
 *      Disable vertical dragging of the element
 *      <br><br>
 *      <li><b>disableHorizontal</b> (boolean) [false] </li>
 *      Disable horizontal dragging of the element
 *      <br><br>
 *      <li><b>escapeToCancel</b> (boolean) [true] </li>
 *      Whether or not pressing the Escape key while dragging should 
 *      cancel the drag
 *      <br><br>
 *      <li><b>onStart</b> (function) [null] </li>
 *      Function to run at the beginning of a drag<br>
 *        - Takes no parameters
 *      <br><br>  
 *      <li><b>onDrag</b> (function) [null] </li>
 *      Function to run with each onmousemove event during a drag<br>
 *        - Takes no parameters
 *      <br><br>  
 *      <li><b>onStop</b> (function) [null] </li>
 *      Function to run at the end of a successful drag<br>
 *        - Takes a boolean parameter indicating whether this stop 
 *          was caused by a user cancel or not
 *    </ul>
 */
org.brophy.js.drag.Dragger = function(options)
{
  /**
   * @private
   */
  this._init = function()
  {
    var MS = MochiKit.Style;
    this._mouseup = null;     // onmouseup id
    this._mousemove = null;   // onmousemove id
    this._keyup = null;       // onkeydown id
    this._original = null;    // Element clicked on
    this._target = null;      // Element we are dragging
    this._event = null;       // The event that was fired.
    this._offset = null;      // Offset from mouse
    this._moved = false;      // Have we moved at all?
    this._originalStyle = {}; // Store the original styles
    this._originalPosition = null;             // Store the original position
    this._snapDiff = new MS.Coordinates(0,0);  // Store the snap difference
  }

  this._defaultOptions = 
  {
    /* Function to create a cloned node to actually drag.  Should 
       accept a MochiKit event object as a parameter, and return a 
       node that has been inserted into the DOM */
    'getClonedNode' : null,

    /* Simple option to just clone the node */
    'cloneNode' : false,

    /* Remove the dragged element from the DOM when we finish? */
    'removeElement' : false,
  
    /* Opacity value between 0 and 1 for the dragged element */
    'opacity' : 0.5,

    /* Snap the element every 'n' pixels */
    'snap' : 1,

    /* Disable vertical dragging */
    'disableVertical' : false,

    /* Disable horizontal dragging */
    'disableHorizontal' : false,

    /* Escape to Cancel */
    'escapeToCancel' : true,

    /* Function to run at the beginning of a drag
        - Takes no parameters
        - Should return the updated element */
    'onStart' : null,
    
    /* Function to run with each onmousemove event during a drag
        - Takes no parameters
        - Should return the updated element */
    'onDrag' : null,
    
    /* Function to run at the end of a successful drag
        - Takes no parameters
        - Should return the updated element */
    'onStop' : null,
    
    /* Additional options?
        - More useful defaults?
          - Cloning boolean option?
        - Click Validator?
        - Handles for Dragging?
        - Separate resizing library? 
        
        
    */
    '_' : null
  };

  /**
   * @private
   */
  this._checkFuncEffects = function(funcName)
  {
    if(MochiKit.Base.isUndefinedOrNull(this._target))
    {
      throw("org.brophy.js.drag.js: The specified " + funcName + 
            " function must return the new element.");
    }
    return true;
  };

  /**
   * @private
   */
  this._keyupfunc = function(e)
  {
    if(this.escapeToCancel && e.key().string === "KEY_ESCAPE")
      { this._stop(e,true); }
  };

  /**
   * @private
   */
  this._initialDiff = function(lhs, rhs)
  {
    var MS = MochiKit.Style;
    var newX = Math.round(lhs.x - rhs.x);
    var newY = Math.round(lhs.y - rhs.y);
    return new MochiKit.Style.Coordinates(newX, newY)
  };

  this._diff = function(lhs, rhs)
  {
    var MS = MochiKit.Style;
    var newX = Math.round(lhs.x - rhs.x);
    var newY = Math.round(lhs.y - rhs.y);

    return new MochiKit.Style.Coordinates(
      (this.disableHorizontal) ?
        MS.getElementPosition(this._target).x :
        newX - (newX%this.snap) + this._snapDiff.x,
      (this.disableVertical) ?
        MS.getElementPosition(this._target).y :
        newY - (newY%this.snap) + this._snapDiff.y );

  };

  /**
   * @private
   */
  this._drag = function(e)
  {
    var MS = MochiKit.Style;
    this._event = e;

    if(this._target != this._original && !this._moved)
      { MS.setStyle(this._target, {'position' : 'absolute'}); }

    MS.setElementPosition(this._target,
                          this._diff(e.mouse().page, this._offset));

    if(this._target == this._original && !this._moved)
      { MS.setStyle(this._target, {'position' : 'absolute'}); }

    this._moved = true;

    if(!MochiKit.Base.isUndefinedOrNull(this.onDrag)) { this.onDrag(); }

    // Prevent the drag from highlighting any text
    org. brophy.js.util.deselectAll();

    // Prevent the event from propogating
    e.stop();
    this._event = null;
  };

  /**
   * @private
   */
  this._stop = function(e, cancel)
  {
    this._event = e; 
    var MS = MochiKit.Style;
    var MD = MochiKit.DOM;
    var MB = MochiKit.Base;

    cancel = MB.isUndefinedOrNull(cancel) ? false : cancel;

    MochiKit.Signal.disconnect(this._mousemove);
    MochiKit.Signal.disconnect(this._mouseup);  
    MochiKit.Signal.disconnect(this._keyup);  
      
    // If we are supposed to remove, or if we created a new element 
    // and didn't move it at all we need to get rid of it as it 
    // is currently sitting directly on top of us.
    if(cancel)
    {
      if(this._target != this._original)
      {
        MD.removeElement(this._target);
      }
      else
      {
        this._resetTarget();
 
        // We didn't clone, put us back where we started
        MS.setElementPosition(this._target, this._originalPosition);
      }
    }
    else if( this.removeElement || 
           (!this._moved && this._original != this._target) )
    {
      MD.removeElement(this._target);
    }
    else
    {
      this._resetTarget();
    }

    // Run the user specified onStop function
    if(!MB.isUndefinedOrNull(this.onStop)) { this.onStop(cancel); }
    this._event = null;
    this._moved = false;
  };

  /**
   * @private
   */
  this._resetTarget = function()
  {
    var MS = MochiKit.Style;
    var MD = MochiKit.DOM;
    var MB = MochiKit.Base;

    // Reset any original attributes about the object

    /* Handle opacity by itself due to cross browser issues */
    if(!MB.isUndefinedOrNull(this._originalStyle.opacity))
    {
      MS.setOpacity(this._target, this._originalStyle.opacity);
      delete(this._originalStyle.opacity);
    }
    MS.setStyle(this._target, this._originalStyle);   
  };

  /**
   * @private
   */
  this._start = function(e)
  {
    this._event = e; 
    var MS = MochiKit.Style;
    var MD = MochiKit.DOM;
    var MB = MochiKit.Base;

    // We need to remember what we're dragging
    this._original = e.src();


    // Cache the offset
    this._offset = this._initialDiff(e.mouse().page,
                                     MS.getElementPosition(this._original));

    // Store the actual element we will drag
    this._target = !MB.isUndefinedOrNull(this.getClonedNode) ?
                     this.getClonedNode(e) : 
                     this.cloneNode ? 
                       this._original.cloneNode(true) : 
                       this._original;

    this._checkFuncEffects("getClonedNode");

    if(MB.isUndefinedOrNull(this._target.parentNode))
    { 
      MD.appendChildNodes(document.body, this._target);
    }

    // If we are not dragging the _original object, we need to set the 
    // position of the new object.
    if(this._target != this._original)
    {
      var pos = MS.getElementPosition(this._original);
      MS.setElementPosition(this._target, pos);
    }
     
    // Cache the snap offset
    if(this.snap > 1)
    {
      var tmpPos = MS.getElementPosition(this._target);
      this._snapDiff = new MS.Coordinates(tmpPos.x%this.snap, tmpPos.y%this.snap);
    }

    // Store any current information about the object
    this._originalStyle.opacity = 
      MS.getStyle(this._target, "opacity");
    this._originalPosition = MS.getElementPosition(this._original);

    // Set any new information about the object
    MS.setOpacity(this._target, this.opacity);   

    if(!MB.isUndefinedOrNull(this.onStart)) { this.onStart(); }

    // Cache the offset from the new element, in case the user changed it's position
    //this._offset = this._diff(e.mouse().page, 
    //                          MS.getElementPosition(this._target)); 

    this._mousemove = MochiKit.Signal.connect(document, 'onmousemove', this._drag); 
    this._mouseup =   MochiKit.Signal.connect(document, 'onmouseup', this._stop); 
    this._keyup =   MochiKit.Signal.connect(document, 'onkeyup', this._keyupfunc); 
    this._event = null;
  }

  this._init();
  MochiKit.Base.setdefault(this,options);
  MochiKit.Base.setdefault(this,this._defaultOptions);
  MochiKit.Base.bindMethods(this);
}


/**
 * @member org.brophy.js.drag.Dragger
 * @description 
 *   Apply a given Dragger object to the specified element.  This will 
 *   make the specified element draggable with all of the properties of 
 *   the given Dragger object.  You may set multiple elements draggable 
 *   with the same Dragger and they will all behave the same.
 *
 * @param   elem  The element to make draggable
 * @returns null
 */
org.brophy.js.drag.Dragger.prototype.setElementDraggable = function(elem)
{
  return MochiKit.Signal.connect(elem, "onmousedown", this._start);
};

/**
 * Get the original node of the drag
 * @returns The original node that was clicked on to start the drag
 * @type Node
 */
org.brophy.js.drag.Dragger.prototype.getOriginalNode = function()
{
  return this._original;
};

/**
 * Get the target node of the drag
 * @returns The actual node we are dragging, which may or may not the 
 * the same as the original, based on the behavior of any specified 
 * 'getClonedNode' function.
 * @type Node
 */
org.brophy.js.drag.Dragger.prototype.getTargetNode = function()
{
  return this._target;
};

/**
 * Get the MochiKit Event that was fired
 * @returns The MochiKit Event that was fired
 * @type MochiKit Event object
 */
org.brophy.js.drag.Dragger.prototype.getEvent = function()
{
  return this._event;
};


